'''
	Script used to install Xbox game artwork.
	v3.2
'''
import datetime
import io
import re
import requests
import traceback
import zipfile
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
from binascii import unhexlify
from BeautifulSoup import *
from difflib import SequenceMatcher as Fuzzing
from itertools import islice
from os import getcwd, listdir, makedirs, rename, remove, stat, walk
from os.path import basename, isdir, isfile, join, exists
from random import randint
from shutil import copy2, copytree, rmtree
from struct import unpack, calcsize
from time import sleep, clock
from xbmc import executebuiltin, getInfoLabel
from xbmcgui import Dialog, DialogProgress

def extract(zip,path):
	with zipfile.ZipFile(zip, 'r') as zip:
		zip.extractall(path)

def download(URL, Dest, Filename, Percent):
	zip_data = ''
	timeout_count = 0
	URL = 'https://www.xbmc4gamers.co.uk/X4G-XEmu/artwork/' + requests.utils.quote(URL)
	
	# XBMC4Gamers.co.uk attempt time out if request is blocked
	timeout = 10
	max_retries = 3

	for _ in range(max_retries):
		if pDialog.iscanceled():
			break

		try:
			response = requests.Session().get(
				URL,
				headers={"User-Agent": "Mozilla/5.0"},
				stream=True,
				verify=False,
				timeout=timeout
			)

			if str(response.status_code).startswith('2'):
				if '.zip' in URL:
					pDialog.update(Percent, Filename, Label_4)
				zip_data = save_response_content(response, Dest, Filename)
				if zip_data:
					break

			if str(response.status_code).startswith('404'):
				break

			if str(response.status_code).startswith('429'):
				timeout_count = timeout
				for _ in range(timeout):
					if pDialog.iscanceled():
						break
					if '.zip' in URL:
						pDialog.update(0, Filename, 'Retrying in: {}'.format(timeout_count))
					time.sleep(1)
					timeout_count -= 1
				timeout += 5

		except requests.exceptions.Timeout:
			print("\nERROR: Timeout\n\tRetrying...")
			timeout += 5

		except requests.exceptions.RequestException as e:
			print("\nERROR: {}\n\tRetrying...".format(e))
			timeout += 5

	return zip_data

def save_response_content(Response,Dest,Filename):
	chunk_size = 256 * 1024
	zipData = io.BytesIO()
	for chunk in Response.iter_content(chunk_size):
		if chunk:  # filter out keep-alive new chunks
			zipData.write(chunk)

	if Filename == "info.txt":
		with open(Dest,'wb') as f:
			f.write(zipData.getvalue())

	return zipData

def process_artwork(Game_Directory, ArtworkFolder, Game_Folder):
		Source_Poster = join(Game_Directory,'_resources\\artwork\\poster.jpg')
		Source_Thumb = join(Game_Directory,'_resources\\artwork\\thumb.jpg')
		Source_Fanart = join(Game_Directory,'_resources\\artwork\\fanart.jpg')
		Source_Video = join(Game_Directory,'_resources\\media\\preview.mp4')
		Game_TBN = join(Game_Directory,'default.tbn')
		Game_Thumb = join(Game_Directory,'icon.png')
		Game_Fanart = join(Game_Directory,'fanart.jpg')
		zipData = ''
		
		remove_files_and_folders(Game_Directory)
		
		rando = randint(10, 40)
		zipData = download(ArtworkFolder+'/_resources.zip','',Game_Folder.upper(),rando)

		if zipData:
			extract(zipData,Game_Directory)
			if isfile(Source_Poster): copy2(Source_Poster,Game_TBN)
			if isfile(Source_Thumb): copy2(Source_Thumb,Game_Thumb)
			if isfile(Source_Fanart): copy2(Source_Fanart,Game_Fanart)
		
		if Video_Install:
			rando = randint(40, 65)
			zipData = download(ArtworkFolder+'/_video.zip','',Game_Folder.upper(),rando)
				
			if zipData:
				extract(zipData,Game_Directory)
				pDialog.update(rando,Game_Folder.upper(),Label_3)
		
		if Large_Fanart:
			rando = randint(70, 90)
			zipData = download(ArtworkFolder+'/_fanart.zip','',Game_Folder.upper(),rando)
				
			if zipData:
				extract(zipData,Game_Directory)
		
		if not Video_Install and not Large_Fanart:
			rando = randint(40, 95)
			pDialog.update(rando,Game_Folder.upper(),Label_3)
		
		pDialog.update(100,Game_Folder.upper(),Label_3)		

def remove_files_and_folders(Sub_Folder):
	To_Remove = [
		'_resources',
		'default.tbn',
		'fanart.jpg',
		'icon.jpg',
		'icon.png'
	]
	
	for Path in To_Remove:
		Full_Path = join(Sub_Folder, Path)
		if isdir(Full_Path):
			rmtree(Full_Path)
		elif isfile(Full_Path):
			remove(Full_Path)

def call_exception(Exception):
	exception_type = type(error).__name__
	exception_message = str(error)
	stack_trace = traceback.format_exc().splitlines()

	exceptionarray = []
	exceptionarray.append("An error of type {} occurred. Arguments:\n{}\n".format(exception_type, exception_message))
	exceptionarray.append("Stack trace:")
	for line in stack_trace:
		exceptionarray.append("\t" + line)
	
	dialog.textviewer('ERROR', "\n".join(exceptionarray))

def internet_check():
	try:
		_ = requests.head('https://www.xbmc4gamers.co.uk/', timeout=3, verify=False)
	except requests.ConnectionError:
		raise Exception("No Interweb access. Please check your network settings")

def get_artwork_menu():
	options = [
		"SELECT ARTWORK TYPE",
		"SELECT ARTWORK",
		"INSTALL ARTWORK",
		"CANCEL"
	]
	return options

def main_menu():
	var = None
	var2 = None  

	while True:
		xbmc.executebuiltin('Dialog.Close(1100)')
		executebuiltin('Skin.SetBool(MenuVisible)')
		title = "MANUAL INSTALL"
		menu = get_artwork_menu()
		choice = Dialog().select(title, menu)

		if choice == -1 or menu[choice] == "CANCEL":
			return
		elif menu[choice] == "SELECT ARTWORK TYPE":
			var = show_artwork_types()
			if not var:
				continue
		elif menu[choice] == "SELECT ARTWORK":
			if var:
				var2 = select_artwork(*var)
				if not var2:
					continue
			else:
				Dialog().ok("Error", "Select artwork type first.")
		elif menu[choice] == "INSTALL ARTWORK":
			if var2:
				installed = install_artwork(*var2)
				if not installed:
					continue
				else:
					return
			else:
				Dialog().ok("Error", "Select artwork first.")

def show_artwork_types():
	Sorted_Artwork_Types = sorted([x.upper() for x in Artwork_Types])  
	Artwork_List = Dialog().select("ARTWORK TYPES", Sorted_Artwork_Types)

	if Artwork_List == -1:
		return None

	if int(Total_Memory) > 64 and getInfoLabel('Skin.HasSetting(ArtworkInstallerFanart)'):
		Large_Fanart = 1
	else:
		executebuiltin('Skin.Reset(ArtworkInstallerFanart)')
	if getInfoLabel('Skin.HasSetting(ArtworkInstallerVideo)'):
		Video_Install = 1

	RootFolder = join(Sorted_Artwork_Types[Artwork_List].lower())

	if '(' in RootFolder and ')' in RootFolder:
		RootFolder = RootFolder.split('(')[1].split(')')[0]

	pattern = r'' + re.escape(RootFolder) + '\\\\(.*)'
	with open(ArtworkMasterList, 'r') as file:
		Read_Artwork_List = re.findall(pattern, file.read())

	Normalized_Directories = sorted([x.lower() for x in Read_Artwork_List])

	return Artwork_List, Normalized_Directories, RootFolder

def select_artwork(Artwork_List, Normalized_Directories, RootFolder):
	if not Normalized_Directories:
		Dialog().ok("Error", "No artwork directories available.")
		return None

	Sorted_Directories = sorted([x.upper() for x in Normalized_Directories])  

	Folder_Content = Dialog().select("AVAILABLE {} ARTWORK".format(RootFolder.upper()), Sorted_Directories)

	if Folder_Content == -1:
		return None

	Folder_Content = join(RootFolder, Normalized_Directories[Folder_Content]).replace('\\', '/')

	return Folder_Content, Normalized_Directories

def install_artwork(Folder_Content, Normalized_Directories):
	if not Normalized_Directories:
		Dialog().ok("Error", "No game directories available.")
		return False

	sorted_game_directories = sorted(Game_Directories, key=lambda s: s.upper())
	sorted_game_directories_display = [directory.upper() for directory in sorted_game_directories]

	GameDirectory_Index = Dialog().select("SOURCES\n{}".format(Folder_Content.split('/')[1].upper()), sorted_game_directories_display)
	if GameDirectory_Index == -1:
		return False

	GameDirectory = sorted_game_directories[GameDirectory_Index]
	Game_Folder = basename(Folder_Content.upper())

	game_folder_candidates = listdir(GameDirectory)
	sorted_game_folder_candidates = sorted(game_folder_candidates, key=lambda s: s.upper())
	sorted_game_folder_candidates_display = [folder.upper() for folder in sorted_game_folder_candidates]

	GameFolder_Index = Dialog().select("SELECT FOLDER FOR\n{}".format(Game_Folder.upper()), sorted_game_folder_candidates_display)
	if GameFolder_Index == -1:
		return False

	GameFolderContent = join(GameDirectory, sorted_game_folder_candidates[GameFolder_Index])

	if isfile(join(GameFolderContent, 'default.xbe')):
		with open(ArtworkMasterList, 'r') as file:
			ArtworkFolder = Folder_Content.lower()
			pDialog.create(Label_1, "", Label_2)
			process_artwork(GameFolderContent, ArtworkFolder, Game_Folder)
			sleep(0.5)
			pDialog.close()
			Dialog().ok(Label_1, "", Label_9 + Game_Folder)
			return True
	else:
		Dialog().ok(Label_6, "", Label_10)
		return False

if __name__ == "__main__":
	# print("Arguments:", sys.argv)
	args = sys.argv[1:] + [0]*3
	arg_standalone, Video_Install, Large_Fanart = [int(arg) for arg in args[:3]]
	custompath_args_str = args[3:]
	Custom_Game_Directories = [arg for arg in custompath_args_str if arg != 0]
	
	dialog = Dialog()
	pDialog = DialogProgress()
	pDialog.update(0)
	executebuiltin('Skin.SetString(DisableCancel,1)')
	executebuiltin('Skin.SetBool(MenuVisible)')
	Total_Memory = getInfoLabel('System.memory(total_no_mb)')
	Working_Directory = getcwd()
	ArtworkPath = join(Working_Directory,'Artwork')
	ArtworkMasterList = join(Working_Directory, 'Artwork Info.txt')
	Artwork_Types = [ "Arcade", "Demos", "Emulators", "Apps", "Games (Custom)", "Homebrew", "Games (JPN)", "Games (NTSC)", "Games (PAL)", "Games (Other)", "Games (Specials)"]
	Folder_Info = ""
	duration = ""
	# Labels
	if arg_standalone:
		Label_1 = ""
	else:
		Label_1 = "Artwork Installer Online"
	Label_2 = "Initialising please wait..."
	Label_3 = "Installing"
	Label_4 = "Downloading"
	Label_6 = "Error"
	Label_7 = "Select a valid directory."
	Label_9 = "Artwork installed for "
	Label_10 = "This folder is empty "
	Label_11 = "Downloading latest[CR][B]Artwork Info.txt[/B]"
	Game_Directories = []
	Game_Directories_E = []
	Game_Directories_F = []
	Game_Directories_G = []

	if not xbmc.getInfoLabel('system.totalspace(E)').split(' ',2)[1] == "Unavailable": Game_Directories_E = [ "E:\\Demos\\", "E:\\Emulators\\", "E:\\Games\\", "E:\\Homebrew\\", "E:\\Ports\\", "E:\\Apps\\", "E:\\Applications\\" ]
	if not xbmc.getInfoLabel('system.totalspace(F)').split(' ',2)[1] == "Unavailable": Game_Directories_F = [ "F:\\Demos\\", "F:\\Emulators\\", "F:\\Games\\", "F:\\Homebrew\\", "F:\\Ports\\", "F:\\Apps\\", "F:\\Applications\\" ]
	if not xbmc.getInfoLabel('system.totalspace(G)').split(' ',2)[1] == "Unavailable": Game_Directories_G = [ "G:\\Demos\\", "G:\\Emulators\\", "G:\\Games\\", "G:\\Homebrew\\", "G:\\Ports\\", "G:\\Apps\\", "G:\\Applications\\" ]

	if arg_standalone:
		Custom_Game_Directories = [ xbmc.getInfoLabel('Skin.String(CustomPath1Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath2Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath3Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath4Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath5Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath6Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath7Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath8Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath9Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath10Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath11Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath12Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath13Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath14Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath15Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath16Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath17Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath18Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath19Selected)'), xbmc.getInfoLabel('Skin.String(CustomPath20Selected)')]

	# Check if sources exist and if they do they aren't empty
	for dir_check in filter(None, Game_Directories_E + Game_Directories_F + Game_Directories_G + Custom_Game_Directories):
		if exists(dir_check) and listdir(dir_check):
			Game_Directories.append(dir_check)

	try: # check if you have Insignia DNS set and a second DNS as it's required
		if getInfoLabel('Network.DNS1Address') == "46.101.64.175" and getInfoLabel('Network.DNS2Address') == "0.0.0.0":
			raise Exception("Insignia DNS detected. You have not set a secondary DNS.")
		try:
			internet_check()
	#
	#	Start of main code.
	#
			try:
				start_time = datetime.datetime.now()
				try:
					check_timestamp = datetime.datetime.fromtimestamp(stat(ArtworkMasterList).st_mtime)
					duration = start_time - check_timestamp
				except: duration = "bypass"
				if duration == "bypass" or duration > datetime.timedelta(minutes=10):
					download('info.txt',ArtworkMasterList,'info.txt',0)

				with open(ArtworkMasterList, 'r') as InputArtworkFile:
					ArtworkTotal = InputArtworkFile.readline().replace('Artwork Total = ','').strip()

				executebuiltin('Skin.Reset(DisableCancel)')
				executebuiltin('Dialog.Close(1100)')
				main_menu()
			except Exception as error:
				pDialog.close()
				call_exception(error)
		
		except Exception as networkissue:
			xbmc.executebuiltin('Dialog.Close(1100)')
			dialog.textviewer('NETWORK CHECK FAILED','''No Internet access or www.xbmc4gamers.co.uk is down.
			
Please check your network settings or make sure you have an Ethernet cable/WIFI adaptor connected to a modem/PC (sharing) or router.

Internet access is required for this version of the Artwork Installer.''')

	except Exception as dnsissue:
		xbmc.executebuiltin('Dialog.Close(1100)')
		dialog.textviewer('INSIGNIA DNS DETECTED', '''You do not have a secondary DNS set.
		
The Artwork Installer will not function without it being set.
		
You can fix this by setting the DNS settings under the Microsoft dashboard, XBMC4Gamers/XBMC-Emustation network settings, or UnleashX network settings.

When you have done this rerun the Artwork Installer again and you should be good :D''')

executebuiltin('Skin.Reset(MenuVisible)')